使用 Alibaba Druid 进行 SQL 翻译 | 您所在的位置:网站首页 › Druid sql执行 › 使用 Alibaba Druid 进行 SQL 翻译 |
原文地址:https://alphahinex.github.io/2022/05/15/use-druid-to-transform-sql/ coverdescription: "Alibaba Druid 不仅仅是一个连接池组件" date: 2022.05.15 10:34 categories: - Java - Database tags: [MySQL, H2, Java, Database, Druid] keywords: MySQL, H2, druid, parser, converter, SQL, SQLUtils Alibaba Druid logoAlibaba Druid 是阿里云计算平台 DataWorks 团队出品,为监控而生的数据库连接池。 Apache 旗下也有一个 Apache Druid,是一个高性能的实时分析数据库。 本文提到的 Druid,指 Alibaba Druid。 Druid 其实是一个 JDBC 组件库,不仅包含数据库连接池组件,还有 SQL Parser 等组件,被大量业务和技术产品使用或集成,从 GitHub 的 Start 数量(25.5k),和 Used by 数量(206k)可见一斑。 github网上关于 Druid 的文档,大多是数据库连接池组件的。SQL Parser 组件的文档,目前以官网 wiki 中的内容为主。 SQL ParserSQL Parser 文档对此组件进行了简单清晰的介绍,重点内容如下: Druid的 sql parser 是目前支持各种数据语法最完备的 SQL Parser。目前对各种数据库的支持如下: 数据库 DML DDL odps 完全支持 完全支持 mysql 完全支持 完全支持 postgresql 完全支持 完全支持 oracle 支持大部分 支持大部分 sql server 支持常用的 支持常用的ddl db2 支持常用的 支持常用的ddl hive 支持常用的 支持常用的ddldruid 还缺省支持 sql-92 标准的语法,所以也部分支持其他数据库的 sql 语法。 Druid SQL Parser 分三个模块: Parser:parser 是将输入文本转换为 ast(抽象语法树),parser 有包括两个部分,Parser 和Lexer,其中Lexer实现词法分析,Parser实现语法分析。 AST:AST是Abstract Syntax Tree的缩写,也就是抽象语法树。AST是parser输出的结果。 Visitor:Visitor是遍历AST的手段,是处理AST最方便的模式,Visitor是一个接口,有缺省什么都没做的实现VistorAdapter。Druid SQL Parser 的使用场景 MySql SQL全量统计 Hive/ODPS SQL执行安全审计 分库分表SQL解析引擎 数据库引擎的SQL Parser还有一些更具体的场景,比如: SQL 格式化 SQL 添加条件 SQL 移除条件 SQL 参数化 SQL 翻译 SQL 翻译SQL-92、SQL-99 等都是标准 SQL,mysql/oracle/pg/sqlserver/odps 等都是方言,也就是dialect。parser/ast/visitor 都需要针对不同的方言进行特别处理。—— 方言 SQL 翻译,即将一种方言,翻译成另一种。比如输入 MySQL 的 SQL 脚本,使用 MySQL 的 Parser 进行解析,再使用 Oracle 的 Visitor 进行遍历输出,就可以完成 MySQL 脚本到 Oracle 脚本的翻译: List sqlStatements = SQLUtils.parseStatements(mysqlSql, DbType.mysql); String oracleSql = SQLUtils.toSQLString(sqlStatements, DbType.oracle); System.out.println(oracleSql);然而在执行类似上面的代码片段进行 SQL 翻译时,你可能会遇到类似下面的报错: java.lang.IllegalArgumentException: not support visitor type : com.alibaba.druid.sql.dialect.oracle.visitor.OracleOutputVisitor at com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlStatementImpl.accept0(MySqlStatementImpl.java:37) at com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlHintStatement.accept0(MySqlHintStatement.java:42) at com.alibaba.druid.sql.ast.SQLObjectImpl.accept(SQLObjectImpl.java:49) at com.alibaba.druid.sql.SQLUtils.toSQLString(SQLUtils.java:436) at com.alibaba.druid.sql.SQLUtils.toSQLString(SQLUtils.java:364) at com.alibaba.druid.sql.SQLUtils.toSQLString(SQLUtils.java:356)这种时候,想完成翻译的动作,就需要付出一些努力了。下面通过一个实例,来看一下如何将 MySQL 的脚本翻译成 H2 可用的脚本。 MySQL 脚本翻译成 H2 脚本在当前最新的 1.2.8 Release 版本中,H2OutputVisitor.java 针对 H2 的方言处理并不多,直接进行翻译时,大概率会遇到类似上面的报错,或翻译出来的结果无法在 H2 中执行。 此时需要参照官方文档 实现自己的Visitor(或如下示例),在 Visitor 中针对 H2 方言进行处理,如: public class CustomH2OutputVisitor extends H2OutputVisitor { public CustomH2OutputVisitor(Appendable appender) { super(appender); } public CustomH2OutputVisitor(Appendable appender, DbType dbType) { super(appender, dbType); } public CustomH2OutputVisitor(Appendable appender, boolean parameterized) { super(appender, parameterized); } public boolean visit(SQLCreateDatabaseStatement x) { /* https://h2database.com/html/commands.html#create_schema CREATE SCHEMA [ IF NOT EXISTS ] { name [ AUTHORIZATION ownerName ] | [ AUTHORIZATION ownerName ] } [ WITH tableEngineParamName [,...] ] */ printUcase("CREATE SCHEMA "); if (x.isIfNotExists()) { printUcase("IF NOT EXISTS "); } x.getName().accept(this); return false; } }可参照下方示例,或 文档 使用自己实现的 Visitor: String sql = "CREATE SCHEMA hinex;CREATE TABLE hinex.employees (jobTitle VARCHAR2(50));CREATE FULLTEXT INDEX hinex.jobTitle USING BTREE ON hinex.employees(jobTitle);"; List stmtList = SQLUtils.parseStatements(sql, DbType.mysql); StringBuilder out = new StringBuilder(); SQLASTOutputVisitor visitor = new CustomH2OutputVisitor(out); for (SQLStatement stmt : stmtList) { stmt.accept(visitor); visitor.println(); } System.out.println(out);运行上面代码,会得到如下结果: CREATE SCHEMA hinex; CREATE TABLE hinex.employees ( jobTitle VARCHAR2(50) ); CREATE INDEX hinex.jobTitle ON hinex.employees (jobTitle);然而在逐步提升方言的翻译能力时,你会发现有些问题无法仅通过扩展 Visitor 来实现,比如上面提到的 java.lang.IllegalArgumentException: not support visitor type : com.alibaba.druid.sql.dialect.oracle.visitor.OracleOutputVisitor 这个报错,是由 MySqlStatementImpl.java:37 抛出的: @Override protected void accept0(SQLASTVisitor visitor) { if (visitor instanceof MySqlASTVisitor) { accept0((MySqlASTVisitor) visitor); } else { throw new IllegalArgumentException("not support visitor type : " + visitor.getClass().getName()); } }在这个 MySQL 的 Statement 中,仅支持通过 MySqlASTVisitor 进行遍历,遇到其他类型的 Visitor 都会抛出异常。如果我们是想将 MySQL 方言翻译成其他的方言,就需要修改这个类使其支持其他类型的 Visitor。 这类修改就不仅仅是扩展 Druid 了,而是要覆盖它的默认行为。这种覆盖类的方式,在 Override same class 中有所讨论: 如果我们能够将定制的类以 class 的形式放到发布包中,并且发布包是依赖 Servlet 容器运行的,依照 Servlet 规范中的要求及推荐,以及 Tomcat 的具体实现,能够实现覆盖; 但如果我们的目标是提供一个基础类库,供其他项目依赖,即定制的类也是在 JAR 包里时,就不好办了,因为原版 Druid JAR 包中,和我们基础类库的 JAR 包中,会存在相同的类。怎么办?提供两种方法。 Pull Request可以将扩展内容直接通过 Pull Request 提交给 Druid,如 #4777 和 #4778,待代码合并进主干,并且 Druid 发布包含这些 PR 内容的新版本后,就可以直接使用了。 maven-assembly-plugin当然,上面那种方法的流程可能会比较长,如果等不及,可以在定制的类库中,使用 maven-assembly-plugin 来将所有的依赖发布成一个定制版的 Druid JAR 包,在使用时用定制版替代原版即可。 在 pom.xml 中添加类似下面的内容: org.apache.maven.plugins maven-assembly-plugin false jar-with-dependencies make-assembly package single这样在执行 mvn package 打包的时候,就会构建出一个包含了所有依赖的版本。通过 install 或 deploy 等命令将定制版 Druid 发布到 Maven 仓库中,就可以被其他项目所使用了。 注意:仅发布包含依赖版本时,需设置上面的 appendAssemblyId 属性为 false,否则会发布包含依赖和不包含依赖两个版本。 在 MySQL 脚本转 H2 中介绍过一个 mysql2h2-converter 项目,其 v0.2.2 版本是使用 JavaCC 编写的 MySQL Parser。v0.3.0 版本便是基于 Druid 1.2.8 进行的扩展,并提供了可以独立使用的 命令行工具 和可被项目依赖的 定制版本,在需要进行 MySQL 脚本转 H2 脚本操作的时候,可以直接使用。 这里是其中最主要的 H2OutputVisitor.java 和 单元测试 。 |
CopyRight 2018-2019 实验室设备网 版权所有 |